﻿// Written by Tim Grant and Adam Bradford for CS3500, Nov. 2012

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using CustomNetworking;
using BB;
using System.Timers;
using System.IO;
using ListExtensions;
using System.Text.RegularExpressions;

namespace Boggle
{
    /// <summary>
    /// Represents a two-player game of Boggle.  The game supports exactly two players; if one or both 
    /// of the clients disconnects in the middle of the game, the game will end.  Each game is timed,
    /// and will end when the remaining time reaches zero.
    /// 
    /// The ClientConnection objects are responsible for maintaining the connections for each player,
    /// as well as maintaining lists of the unique words and illegal words played by each player.
    /// 
    /// Each time a player submits a word, the BoggleGame will determine whether or not the word is
    /// valid, and will determine if it is a unique word that will count towards the player's score in
    /// accordance with the traditional rules of Boggle.
    /// 
    /// After the game time has elapsed, the final scores are sent to each player, along with lists of
    /// the unique, illegal, and common words played by each player.
    /// </summary>
    public class BoggleGame
    {
        private BoggleBoard board;
        private BoggleServer boggleServer;
        private ClientConnection player1, player2;

        private int timeRemaining;
        private Timer gameTimer;
        private HashSet<string> dictionary;        

        // the set of legal words played by both players
        private SortedSet<string> common = new SortedSet<string>();

        // flag is true if the time > 0 & both clients are connected
        private bool gameInProgress = false;

        private readonly object CallbackLock = new object();
        private readonly object lock1 = new object();

        /// <summary>
        /// Creates and starts a new BoggleGame with the specified board layout.
        /// </summary>
        /// <param name="c1">Player 1 connection.</param>
        /// <param name="c2">Player 2 connection.</param>
        /// <param name="time">The duration of the game in seconds.</param>
        /// <param name="dict">The set of legal words.</param> 
        /// <param name="server">The parent BoggleServer object.</param>
        /// <param name="boardString">Optional string representing the board layout.</param>
        public BoggleGame(int time, HashSet<string> dict, BoggleServer server, string boardString = "")
        {
            dictionary = dict;
            player1 = null;
            player2 = null;
            timeRemaining = time;

            // initialize the game board
            if (boardString != "")
                board = new BoggleBoard(boardString);
            else
                board = new BoggleBoard();

            boggleServer = server;
        }

        public void AddPlayer(ClientConnection player)
        {
            if (player1 == null)
            {
                // assign the new player to player1
                player1 = player;

                // start listening for commands
                player1.SS.BeginReceive(BoggleReceiveCallback, player1);
            }
            else
            {
                // assign the new player to player2
                player2 = player;

                // match the opponents
                player1.opponent = player2;
                player2.opponent = player1;

                // start listening for commands
                player2.SS.BeginReceive(BoggleReceiveCallback, player2);
            }
        }

        /// <summary>
        /// Starts the BoggleGame.
        /// </summary>
        public void StartGame()
        {
            // broadcast the START message to both clients
            string startString = "START " + board.ToString() + " " + timeRemaining + " ";
            player1.SS.BeginSend(startString + player2.Name + "\n", (x, y) => { }, null);
            player2.SS.BeginSend(startString + player1.Name + "\n", (x, y) => { }, null);

            // start the game timer and mark the game as 'in progress'
            gameTimer = new Timer(1000);
            gameTimer.Elapsed += new ElapsedEventHandler(UpdateTimer);
            gameTimer.Start();
            gameInProgress = true;
        }

        /// <summary>
        /// In-game callback for ClientConnections.
        /// </summary>
        private void BoggleReceiveCallback(string s, Exception e, object p)
        {
            lock (CallbackLock)
            {
                ClientConnection player = (ClientConnection)p;
                ClientConnection opponent = player.opponent;
                
                try
                {
                    string command = s.ToUpper();
                    
                    // strip out carriage returns for telnet compatibility
                    command = command.Replace("\r", "");

                    // process valid commands beginning with "WORD"
                    if (command.Substring(0, 5).Equals("WORD ") && gameInProgress)
                    {
                        // get the word and trim off the whitespace
                        string word = command.Substring(5);
                        word = word.Trim();

                        if (word.Length < 3 || common.Contains(word))
                        {
                           // the word isn't long enough, or it has already been played 
                        }
                        // make sure the word is legal
                        else if (board.CanBeFormed(word) && dictionary.Contains(word))
                        {
                            Console.WriteLine("{0} played legal word: {1}", player.Name, word);

                            // if the word cannot be removed from the opponent's list,
                            // add it to the player's list.  otherwise, add it to the list
                            // of common words.
                            if (!opponent.LegalWords.Remove(word))
                            {
                                player.LegalWords.AddUnique(word);                               
                            }
                            else
                            {
                                Console.WriteLine("Common word: " + word);
                                common.Add(word);
                            }
                        }
                        else
                        {
                            Console.WriteLine("{0} played illegal word: {1}", player.Name, word);
                            player.IlegalWords.AddUnique(word);
                        }

                        UpdateScores();
                    }
                    // ignore invalid commands
                    else
                    {
                        player.SS.BeginSend("IGNORING " + s + "\n", (x, y) => { }, null);
                    }

                    // listen for the next command
                    player.SS.BeginReceive(BoggleReceiveCallback, p);

                }
                catch (Exception)
                {
                    // if the game is in progress and one of the clients drops out,
                    // stop the game, notify the surviving client and close its connection
                    if (gameInProgress)
                    {
                        gameInProgress = false;
                        gameTimer.Stop();
                        Console.WriteLine("Lost client connection: {0}", player.Name);

                        // send message to remaining client and close the connection
                        player.opponent.SS.BeginSend("TERMINATED\n", (x, y) => { }, null);
                        player.opponent.SS.CloseSocket(1);

                        boggleServer.EndGame(this);
                    }
                    else
                    {
                        boggleServer.CancelGame(this);
                    }
                }
            }
        }

        /// <summary>
        /// Calculates the scores for both players and sends a SCORE message if either
        /// of the scores has changed.
        /// </summary>
        private void UpdateScores()
        {
            // store the previous scores
            int oldScore1 = player1.score;
            int oldScore2 = player2.score;

            // calculate the latest scores
            player1.score = CalculateScore(player1);
            player2.score = CalculateScore(player2);

            // if either of the scores have changed, send the SCORE command to each client
            if (player1.score != oldScore1 | player2.score != oldScore2 | gameInProgress == false)
            {
                player1.SS.BeginSend("SCORE " + player1.score + " " + player2.score + "\n", (x, y) => { }, null);
                player2.SS.BeginSend("SCORE " + player2.score + " " + player1.score + "\n", (x, y) => { }, null);
            }
        }

        /// <summary>
        /// Calculates and returns the given player's score.
        /// </summary>
        private int CalculateScore(ClientConnection player)
        {
            int newScore = 0;
            foreach (string s in player.LegalWords)
            {
                if (s.Length > 2 & s.Length < 5) newScore += 1;
                else if (s.Length == 5) newScore += 2;
                else if (s.Length == 6) newScore += 3;
                else if (s.Length == 7) newScore += 5;
                else if (s.Length > 7) newScore += 11;
            }

            newScore = newScore - player.IlegalWords.Count;

            return newScore;
        }

        /// <summary>
        /// Generates a string with the post-game summary for the given player.
        /// </summary>
        /// <returns>A post-game summary for the given player.</returns>
        private String GenerateSummary(ClientConnection player)
        {
            ClientConnection opponent = player.opponent;

            // create and return the summary message
            string summary = "STOP ";

            summary += player.LegalWords.Count;
            foreach (string s in player.LegalWords) summary += " " + s;

            summary += " " + opponent.LegalWords.Count;
            foreach (string s in opponent.LegalWords) summary += " " + s;

            summary += " " + common.Count;
            foreach (string s in common) summary += " " + s;

            summary += " " + player.IlegalWords.Count;
            foreach (string s in player.IlegalWords) summary += " " + s;

            summary += " " + opponent.IlegalWords.Count;
            foreach (string s in opponent.IlegalWords) summary += " " + s;

            summary += "\n";

            return summary;
        }

        /// <summary>
        /// Event handler for clock tick events.  Counts down the game timer once every second.
        /// 
        /// When the game is in progress, a "TIME" message is sent to each client which indicates the remaining time.
        /// 
        /// When the time reaches zero, the final scores are transmitted to each client, followed by a "STOP" message
        /// which contains a list of the words used to determine the score.
        /// </summary>
        private void UpdateTimer(object source, ElapsedEventArgs e)
        {
            timeRemaining--;

            if (timeRemaining > 0)
            {
                player1.SS.BeginSend("TIME " + timeRemaining.ToString() + "\n", (x, y) => { }, null);
                player2.SS.BeginSend("TIME " + timeRemaining.ToString() + "\n", (x, y) => { }, null);
            }
            else
            {
                // stop the game
                gameInProgress = false;
                gameTimer.Stop();
                UpdateScores();

                // send the ending summaries and close the connections
                player1.SS.BeginSend(GenerateSummary(player1), FinalMessageSent, player1);
                player2.SS.BeginSend(GenerateSummary(player2), FinalMessageSent, player2);

                // end the game
                boggleServer.EndGame(this);
            }
        }

        /// <summary>
        /// Ensures that the final STOP message is sent before closing the connection.
        /// </summary>
        private void FinalMessageSent(Exception ex, object p)
        {
            lock (lock1)
            {
                ClientConnection player = (ClientConnection)p;
                player.SS.CloseSocket(5);
            }
        }
    }

}